package top.rhynie.web3j;

import com.github.ki5fpl.tronj.abi.FunctionEncoder;
import com.github.ki5fpl.tronj.abi.FunctionReturnDecoder;
import com.github.ki5fpl.tronj.abi.TypeReference;
import com.github.ki5fpl.tronj.abi.datatypes.Address;
import com.github.ki5fpl.tronj.abi.datatypes.Bool;
import com.github.ki5fpl.tronj.abi.datatypes.DynamicArray;
import com.github.ki5fpl.tronj.abi.datatypes.Function;
import com.github.ki5fpl.tronj.abi.datatypes.Type;
import com.github.ki5fpl.tronj.abi.datatypes.Uint;
import com.github.ki5fpl.tronj.abi.datatypes.Utf8String;
import com.google.protobuf.Any;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.util.encoders.Hex;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.tron.common.crypto.ECKey;
import org.tron.common.crypto.Sha256Sm3Hash;
import org.tron.common.utils.ByteArray;
import org.tron.protos.Protocol;
import org.tron.protos.contract.BalanceContract;
import org.tron.protos.contract.SmartContractOuterClass;
import org.tron.walletserver.WalletApi;
import top.rhynie.web3j.entity.Block;
import top.rhynie.web3j.enums.TronApiEnum;
import top.rhynie.web3j.enums.TronNetworkEnum;
import top.rhynie.web3j.utils.GsonUtil;
import top.rhynie.web3j.utils.HttpClientUtil;

import java.math.BigInteger;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;


public class Web3j {

  private String network;

  public String getNetwork() {
    return network;
  }

  public void setNetwork(String network) {
    this.network = network;
  }

  public Web3j() {
    this.network = TronNetworkEnum.MAINNET.getUrl();
  }

  public Web3j(String network) {
    this.network = network;
  }

  /**
   * 根据地址查询账号的信息
   */
  public <T> ResponseEntity<T> getAccountInfo(String address, Class<T> responseType) {
    String url = network + TronApiEnum.ACCOUNT_INFO.getUrl() + address;
    return HttpClientUtil.get(url, responseType);
  }

  /**
   * 查询最新区块信息
   */
  public <T> ResponseEntity<T> getNowBlock(Class<T> responseType) {
    String url = network + TronApiEnum.NOW_BLOCK.getUrl();
    return HttpClientUtil.get(url, responseType);
  }

  /**
   * 根据合约地址，查询区块链中的智能合约信息
   */
  public <T> ResponseEntity<T> getContract(String address, Class<T> responseType) {
    String url = network + TronApiEnum.CONTRACT_INFO.getUrl();
    String hex = ByteArray.toHexString(WalletApi.decodeFromBase58Check(address));
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_JSON);
    Map<String, String> body = new HashMap<>();
    body.put("value", hex);
    HttpEntity entity = new HttpEntity(body, headers);
    return HttpClientUtil.post(url, entity, responseType);
  }

  /**
   * 调用合约的函数，函数类型为view或pure
   */
  public String triggerConstantContract(String ownerAddr, String contractAddr, String functionSelector, String parameter, String returnParameter) {
    String url = network + TronApiEnum.TRIGGER_CONSTANT_CONTRACT.getUrl();

    String functionName = functionSelector.substring(0, functionSelector.indexOf("("));

    List<Type> typeList = Collections.emptyList();
    if(StringUtils.isNotBlank(parameter)) {
      List<String> parameterType = Arrays.asList(functionSelector.substring(functionSelector.indexOf("(") + 1,
          functionSelector.indexOf(")")).split(","));
      List<String> parameterList = Arrays.asList(parameter.split(","));
      typeList = Stream.iterate(0, x -> ++x)
          .limit(parameterList.size())
          .map(x -> {
            if ("address".equals(parameterType.get(x))) {
              return new Address(parameterList.get(x)) {
              };
            } else if ("uint".equals(parameterType.get(x))) {
              return new Uint(new BigInteger(parameterList.get(x))) {
              };
            }
            return null;
          })
          .filter(Objects::nonNull)
          .collect(Collectors.toList());
    }

    List<String> returnParameterType = Arrays.asList(returnParameter.split(","));

    List<TypeReference<?>> returnTypeList = returnParameterType.stream()
        .map(p -> {
          if("bool".equals(p)) {
            return new TypeReference<Bool>() {};
          } else if ("address".equals(p)) {
            return new TypeReference<Address>() {};
          } else if("address[]".equals(p)) {
            return new TypeReference<DynamicArray<Address>>() {};
          } else if("uint".equals(p)) {
            return new TypeReference<Uint>(){};
          } else if("uint[]".equals(p)) {
            return new TypeReference<DynamicArray<Uint>>() {};
          } else if("string".equals(p)) {
            return new TypeReference<Utf8String>() {};
          }
          return null;
        })
        .filter(Objects::nonNull)
        .collect(Collectors.toList());

    Function function = new Function(functionName, typeList, returnTypeList);

    String parameterAbi = FunctionEncoder.encode(function).substring(8);

    String ownerHex = ByteArray.toHexString(WalletApi.decodeFromBase58Check(ownerAddr));
    String contractHex = ByteArray.toHexString(WalletApi.decodeFromBase58Check(contractAddr));
    Map<String, String> body = new HashMap<>();
    body.put("owner_address", ownerHex);
    body.put("contract_address", contractHex);
    body.put("function_selector", functionSelector);
    body.put("parameter", parameterAbi);
    HttpEntity entity = new HttpEntity(body);
    ResponseEntity<String> response = HttpClientUtil.post(url, entity, String.class);

    Map<String, Object> resultMap = GsonUtil.GsonToMaps(response.getBody());

    String result = resultMap.get("constant_result").toString().replace("[", "").replace("]", "");
    List<Type> returnParameterList = FunctionReturnDecoder.decode(result, function.getOutputParameters());
    String returnParameterResult = returnParameterList.stream()
        .map(t -> {
          if (Uint.class.isInstance(t)) {
            return ((Uint) t).getValue().toString();
          }
          if (DynamicArray.class.isInstance(t)) {
            return t.getValue().toString();
          }
          return t.getValue().toString();
        })
        .filter(Objects::nonNull)
        .collect(Collectors.joining(";"));
    return returnParameterResult;
  }

  /**
   * 调用合约的函数，函数类型非view或pure
   */
  public String triggerSmartContract(String privateKey, String ownerAddr, String contractAddr, String functionSelector, String parameter, long trx, long feeLimit) throws InvalidProtocolBufferException {
    byte[] privateBytes = ByteArray.fromHexString(privateKey);
    ResponseEntity<String> nowBlock = this.getNowBlock(String.class);
    Map<String, Object> map = GsonUtil.GsonToMaps(nowBlock.getBody());
    String block_header = map.get("block_header").toString();
    Block block = GsonUtil.GsonToBean(nowBlock.getBody(), Block.class);
    Block.BlockHeader blockHeader = GsonUtil.GsonToBean(block_header, Block.BlockHeader.class);
    blockHeader.convertRawData();

    Long blockTimestamp = blockHeader.getTimestamp();
    Long blockHeight = blockHeader.getNumber();

    byte[] blockHash = Hex.decode(block.getBlockID());
    Protocol.Transaction transaction = createSmartContractTransaction(ownerAddr, contractAddr, functionSelector, parameter, trx, feeLimit, blockTimestamp, blockHeight, blockHash);
    byte[] transactionBytes = transaction.toByteArray();
    byte[] transaction4 = signTransaction2Byte(transactionBytes, privateBytes);

    ResponseEntity<String> broadcast = this.broadcastHex(ByteArray.toHexString(transaction4), String.class);

    return broadcast.getBody();
  }

  public <T> ResponseEntity<T> broadcastHex(String transaction, Class<T> responseType) {
    String url = network + TronApiEnum.BROADCAST_HEX.getUrl();
    Map<String, String> body = new HashMap<>();
    body.put("transaction", transaction);
    HttpEntity entity = new HttpEntity(body);
    return HttpClientUtil.post(url, entity, responseType);
  }

  private Protocol.Transaction createTransaction(byte[] from, byte[] to, long amount, long blockTimestamp, long blockHeight, byte[] blockHash) {
    Protocol.Transaction.Builder transactionBuilder = Protocol.Transaction.newBuilder();

    /*设置合约内部数据*/
    Protocol.Transaction.Contract.Builder contractBuilder = Protocol.Transaction.Contract.newBuilder();
    BalanceContract.TransferContract.Builder transferContractBuilder = BalanceContract.TransferContract.newBuilder();
    transferContractBuilder.setAmount(amount);
    ByteString bsTo = ByteString.copyFrom(to);
    ByteString bsOwner = ByteString.copyFrom(from);
    transferContractBuilder.setToAddress(bsTo);
    transferContractBuilder.setOwnerAddress(bsOwner);
    try {
      Any any = Any.pack(transferContractBuilder.build());
      contractBuilder.setParameter(any);
    } catch (Exception e) {
      return null;
    }
    /*设置备注，交易过期时间等数据*/
    contractBuilder.setType(Protocol.Transaction.Contract.ContractType.TransferContract);
    transactionBuilder.getRawDataBuilder().addContract(contractBuilder).setTimestamp(System.currentTimeMillis())
        .setExpiration(blockTimestamp + 10 * 60 * 60 * 1000);
    Protocol.Transaction transaction = transactionBuilder.build();
    Protocol.Transaction refTransaction = setReference(transaction, blockHeight,blockHash);
    return refTransaction;
  }

  private Protocol.Transaction createSmartContractTransaction(String ownerAddr, String contractAddr, String functionSelector, String parameter, long trx, long feeLimit, long blockTimestamp, long blockHeight, byte[] blockHash) {
    Protocol.Transaction.Builder transactionBuilder = Protocol.Transaction.newBuilder();

    /*设置合约内部数据*/
    Protocol.Transaction.Contract.Builder contractBuilder = Protocol.Transaction.Contract.newBuilder();
    SmartContractOuterClass.TriggerSmartContract.Builder triggerSmartContractBuilder = SmartContractOuterClass.TriggerSmartContract.newBuilder();

    ByteString contract = ByteString.copyFrom(WalletApi.decodeFromBase58Check(contractAddr));
    ByteString bsOwner = ByteString.copyFrom(WalletApi.decodeFromBase58Check(ownerAddr));
    triggerSmartContractBuilder.setContractAddress(contract);
    triggerSmartContractBuilder.setOwnerAddress(bsOwner);

    //封装data
    List<Type> list = Collections.emptyList();
    if(StringUtils.isNotBlank(parameter)) {
      List<String> parameterType = Arrays.asList(functionSelector.substring(functionSelector.indexOf("(") + 1,
          functionSelector.indexOf(")")).split(","));
      List<String> parameterList = Arrays.asList(parameter.split(","));
      list = Stream.iterate(0, x -> ++x)
          .limit(parameterList.size())
          .map(x -> {
            if ("address".equals(parameterType.get(x))) {
              return new Address(parameterList.get(x)) {
              };
            } else if ("uint".equals(parameterType.get(x))) {
              return new Uint(new BigInteger(parameterList.get(x))) {
              };
            }
            return null;
          })
          .filter(Objects::nonNull)
          .collect(Collectors.toList());
    }

    String functionName = functionSelector.substring(0, functionSelector.indexOf("("));
    Function function = new Function(functionName, list, Collections.emptyList());

    byte[] input = Hex.decode(FunctionEncoder.encode(function));
    ByteString data = ByteString.copyFrom(input);
    triggerSmartContractBuilder.setData(data);

    //调用方法时转trx到合约地址
    triggerSmartContractBuilder.setCallValue(trx);
    try {
      Any any = Any.pack(triggerSmartContractBuilder.build());
      contractBuilder.setParameter(any);
    } catch (Exception e) {
      return null;
    }
    /*设置备注，交易过期时间等数据*/
    contractBuilder.setType(Protocol.Transaction.Contract.ContractType.TriggerSmartContract);

    transactionBuilder.getRawDataBuilder().addContract(contractBuilder).setTimestamp(System.currentTimeMillis())
        //setFeeLimit 设置交易消耗的能量
        .setExpiration(blockTimestamp + 10 * 60 * 60 * 1000).setFeeLimit(feeLimit);
    Protocol.Transaction transaction = transactionBuilder.build();
    Protocol.Transaction refTransaction = setReference(transaction, blockHeight,blockHash);
    return refTransaction;
  }

  /*设置参照块数据*/
  private Protocol.Transaction setReference(Protocol.Transaction transaction, long blockHeight, byte[] blockHash) {
    byte[] refBlockNum = ByteArray.fromLong(blockHeight);
    Protocol.Transaction.raw rawData = transaction.getRawData().toBuilder()
        .setRefBlockHash(ByteString.copyFrom(ByteArray.subArray(blockHash, 8, 16)))
        .setRefBlockBytes(ByteString.copyFrom(ByteArray.subArray(refBlockNum, 6, 8))).build();
    return transaction.toBuilder().setRawData(rawData).build();
  }

  private byte[] signTransaction2Byte(byte[] transaction, byte[] privateKey)
      throws InvalidProtocolBufferException {
    ECKey ecKey = ECKey.fromPrivate(privateKey);
    Protocol.Transaction transaction1 = Protocol.Transaction.parseFrom(transaction);
    byte[] rawdata = transaction1.getRawData().toByteArray();
    byte[] hash = Sha256Sm3Hash.hash(rawdata);
    byte[] sign = ecKey.sign(hash).toByteArray();
    return transaction1.toBuilder().addSignature(ByteString.copyFrom(sign)).build().toByteArray();
  }
}
